package com.bryanprogramming.zend.logic;

import com.bryanprogramming.zend.logic.powerups.PowerUp;
import java.awt.Point;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Random;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Scanner;

/**
 *
 * @author charles bryan
 */
public class ZEnd {

    public enum Direction {

        UP, RIGHT, DOWN, LEFT
    };

    private static final Random gen = new Random();
    public static final char EMPTY_TILE = ' ';
    private Tile[][] tiles;
    private ArrayList<Tile> emptyTiles;
    private ArrayList<Tile> filledTiles;
    private Deque<char[][]> undo;
    private Deque<Integer> undoScore;
    private int undoCount = 0;
    private int score = 0;
    private boolean gameWon = false;
    private boolean gameOver = false;
    final int UP = 0, RIGHT = 1, DOWN = 2, LEFT = 3;
    private boolean safeEnd = false;

    public ZEnd() {

    }

    public void startGame() {

        emptyTiles = new ArrayList<>();
        filledTiles = new ArrayList<>();
        undo = new LinkedList<>();
        undoScore = new LinkedList<>();
        undoCount = 5;
        buildTiles();
        addRandomTile();
        addRandomTile();
        updateBoard(toCharArray());
        gameOver = false;
        gameWon = false;
        setUndoStatus(false);
        setUndoCount(undoCount);
        setScore(0);
    }

    public void startGame(char[][] startBoard) {
        startGame();
        setTilesFrom(startBoard);
        updateBoard(toCharArray());
    }

    public void shift(Direction d) {
        if (!gameOver) {
            saveMove();
            boolean moveMade = false;
            switch (d) {
                case UP:
                    moveMade = this.shiftUp();
                    break;
                case DOWN:
                    moveMade = this.shiftDown();
                    break;
                case LEFT:
                    moveMade = this.shiftLeft();
                    break;
                case RIGHT:
                    moveMade = this.shiftRight();
                    break;
            }
            clearMergedStatus();
            if (moveMade) {
                afterMove();
            }
            else {
                removeSavedMove();
            }
        }
    }

    public void usePowerUp(PowerUp pu) {
        pu.usePowerUp();
    }
    
    public void setSafeEnd(boolean enable) {
        this.safeEnd = enable;
    }

    public boolean getUndoStatus() {
        return undo.size() != 0 && undoCount > 0;
    }

    public void undo() {
        if (!gameOver && undoCount > 0) {
            this.setScore(undoScore.removeLast());
            char[][] last = undo.removeLast();
            this.setTilesFrom(last);
            this.updateBoard(last);
            decrementUndoCount(1);
            setUndoStatus(undo.size() != 0 && undoCount > 0);
        }
    }

    public void addUndo(int undoAmount) {
        incrementUndoCount(undoAmount);
        setUndoStatus(undo.size() != 0 && undoCount > 0);
    }

    public void spellWord(Point[] locations) {
        if (locations.length < 4) {
            return;
        }
        StringBuilder word = new StringBuilder(locations.length);
        Tile highestTile = null;
        char highestChar = 'A';
        for (int index = 0; index < locations.length; index++) {
            Tile current = tiles[locations[index].x][locations[index].y];
            if (current.getValue() == EMPTY_TILE) {
                return;
            }
            //is this the last tile? If not, check the neighbor
            if (index < locations.length - 1) {
                Tile next = tiles[locations[index + 1].x][locations[index + 1].y];
                if (!current.isNeighbor(next)) {
                    return;
                }
            }
            //know it is a letter and the next tile is a neighbor
            word.append(current.value);
            if (highestChar < current.value) {
                highestChar = current.value;
                highestTile = current;
            }

        }
        if ((char) (highestTile.value + getIncrementAmount(locations.length)) >= 'Z') {
            return; //can't get to 'Z' by spelling!
        }

        //ok, so we now have the word built, is it in the Dictionary?
        if (Dictionary.isValidWord(word.toString().toLowerCase())) {
            scoreWord(locations, highestTile);
        }
    }

    private void scoreWord(Point[] locations, Tile highestTile) {
        int wordScore = 0;
        char updatedValue = 'A';
        for (Point p : locations) {
            wordScore += tiles[p.x][p.y].value - 65;
            if (tiles[p.x][p.y] != highestTile) {
                tiles[p.x][p.y].value = EMPTY_TILE;
                emptyTiles.add(tiles[p.x][p.y]);
            }
            else {
                updatedValue = (char) (tiles[p.x][p.y].value + getIncrementAmount(locations.length));
                tiles[p.x][p.y].value = EMPTY_TILE;
            }
        }
        addToScore(wordScore);
        updateBoard(toCharArray());

        highestTile.value = updatedValue;
        sendNewTile(highestTile);
        //can't undo past this move!
        undo.clear();
        setUndoStatus(false);
    }

    private int getIncrementAmount(int wordLength) {
        if (wordLength < 4) {
            return 0;
        }
        else if (wordLength == 4) {
            return 1;
        }
        else if (wordLength == 5) {
            return 2;
        }
        else {
            return 3;
        }
    }

    private boolean shiftLeft() {
        boolean moveMade = false;
        for (int row = 0; row < tiles.length; row++) {
            for (int col = 1; col < tiles[row].length; col++) {
                moveMade |= shiftTile(tiles[row][col], LEFT);
            }
        }
        return moveMade;
    }

    private boolean shiftRight() {
        boolean moveMade = false;
        for (int row = 0; row < tiles.length; row++) {
            for (int col = tiles[row].length - 2; col >= 0; col--) {
                moveMade |= shiftTile(tiles[row][col], RIGHT);
            }
        }
        return moveMade;
    }

    private boolean shiftUp() {
        boolean moveMade = false;
        for (int col = 0; col < tiles.length; col++) {
            for (int row = 1; row < tiles[col].length; row++) {
                moveMade |= shiftTile(tiles[row][col], UP);
            }
        }
        return moveMade;
    }

    private boolean shiftDown() {
        boolean moveMade = false;
        for (int col = 0; col < tiles.length; col++) {
            for (int row = tiles[col].length - 2; row >= 0; row--) {
                moveMade |= shiftTile(tiles[row][col], DOWN);
            }
        }
        return moveMade;
    }

    private void afterMove() {
        updateBoard(toCharArray());
        Tile t = addRandomTile();
        sendNewTile(t);
        checkGameStatus();
        sendNewTile(t.getRow(), t.getCol());
    }

    private boolean shiftTile(Tile t, int direction) {
        //base cases
        if (t.getValue() == EMPTY_TILE) {
            return false;
        }
        if (t.neighbors[direction] == null) {
            return false;
        }
        char next = t.neighbors[direction].getValue();
        if (next != EMPTY_TILE && next != t.getValue()) {
            return false;
        }
        //recursive cases
        if (next == EMPTY_TILE) {
            //slide value of tile
            slideTile(t, direction);
            //recursive call on neighbor
            shiftTile(t.neighbors[direction], direction);
            return true;
        }
        else {
            if (!t.neighbors[direction].merged && !t.merged) {
                //merge this tile with neighbor
                mergeTile(t, direction);
                return true;
            }
        }
        return false;
    }

    private void slideTile(Tile t, int direction) {
        Tile neighbor = t.neighbors[direction];
        neighbor.value = t.getValue();
        t.value = EMPTY_TILE;
        emptyTiles.remove(neighbor);
        emptyTiles.add(t);
    }

    private void mergeTile(Tile t, int direction) {
        Tile neighbor = t.neighbors[direction];
        neighbor.value++;
        gameWon |= neighbor.getValue() == 'Z';
        neighbor.merged = true;
        addToScore(neighbor.getValue() - 65);
        t.value = EMPTY_TILE;
        emptyTiles.remove(neighbor);
        emptyTiles.add(t);
    }

    private void checkGameStatus() {
        if (gameWon) {
            setGamePlayStatus(gameWon);
            gameOver = true;
        }
        else if (emptyTiles.isEmpty() && !hasMoreMoves()) {
            if (safeEnd) {
                if(undoCount < 5) {
                    setUndoCount(undoCount = 5);
                    setUndoStatus(getUndoStatus());
                    safeEnd = false;
                }
            }
            else {
                setGamePlayStatus(gameWon);
                gameOver = true;
            }
        }
    }

    private boolean hasMoreMoves() {
        for (int row = 0; row < tiles.length; row++) {
            for (int col = 0; col < tiles[row].length; col++) {
                for (Tile neighbor : tiles[row][col].neighbors) {
                    if (neighbor != null) {
                        if (neighbor.getValue() == tiles[row][col].getValue()) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    private void removeSavedMove() {
        if (undo.size() > 0) {
            undo.removeLast();
            undoScore.removeLast();
            if (undo.size() == 0) {
                setUndoStatus(false);
            }
        }
    }

    private void saveMove() {
        if (undo.size() == 0 && undoCount > 0) {
            setUndoStatus(true);
        }
        undo.add(toCharArray());
        undoScore.add(this.score);
    }

    private void clearMergedStatus() {
        for (int row = 0; row < tiles.length; row++) {
            for (int col = 0; col < tiles[row].length; col++) {
                tiles[row][col].merged = false;
            }
        }
    }

    private Tile addRandomTile() {
        if (emptyTiles.isEmpty()) {
            throw new IllegalStateException("No empty tiles");
        }
        char letter;
        if (gen.nextInt(8) > 0) {
            letter = 'A';
        }
        else {
            letter = 'B';
        }
        Tile emptyTile = this.emptyTiles.remove(gen.nextInt(this.emptyTiles.size()));
        emptyTile.value = letter;
        filledTiles.add(emptyTile);
        return emptyTile;
    }

    private void clearTile(int row, int col) {
        Tile t = new Tile(row, col);
        emptyTiles.add(t);
        filledTiles.remove(t);
    }

    private void buildTiles() {
        tiles = new Tile[4][4];
        for (int row = 0; row < tiles.length; row++) {
            for (int col = 0; col < tiles[row].length; col++) {
                Tile t = new Tile(row, col);
                t.value = EMPTY_TILE;
                emptyTiles.add(t);
                tiles[row][col] = t;
            }
        }
        setlinks();
    }

    private void setlinks() {
        File file = new File("tile_links.txt");
        String line = "";
        try {
            Scanner inFile = new Scanner(file);
            while (inFile.hasNextLine()) {
                
                line = inFile.nextLine();
                String[] parts = line.split(":");
                Tile current = tiles[Character.digit(parts[0].charAt(0), 10)][Character.digit(parts[0].charAt(2), 10)];
                for (int index = 1; index < parts.length; index++) {
                    StringBuilder sb = new StringBuilder();
                    int neighbor = Character.digit(parts[index].charAt(0), 10);
                    int row = Character.digit(parts[index].charAt(2), 10);
                    int col = Character.digit(parts[index].charAt(4), 10);
                    current.neighbors[neighbor] = tiles[Character.digit(parts[index].charAt(2), 10)][Character.digit(parts[index].charAt(4), 10)];
                    sb.append("tiles[" + current.row + "][" + current.col + "].neighbors[" + neighbor + "] = tiles[" + row + "][" + col + "];");
                    System.out.println(sb.toString());
                }
            }
        }
        catch (FileNotFoundException ex) {
            System.err.println("File not read, exiting");
        }

    }
    
    private void setHardLinks() {
        tiles[0][0].neighbors[1] = tiles[0][1];
        tiles[0][0].neighbors[2] = tiles[1][0];
        tiles[0][1].neighbors[1] = tiles[0][2];
        tiles[0][1].neighbors[2] = tiles[1][1];
        tiles[0][1].neighbors[3] = tiles[0][0];
        
        tiles[0][2].neighbors[1] = tiles[0][3];
        tiles[0][2].neighbors[2] = tiles[1][2];
        tiles[0][2].neighbors[3] = tiles[0][1];
        tiles[0][3].neighbors[2] = tiles[1][3];
        tiles[0][3].neighbors[3] = tiles[0][2];
        
        tiles[1][0].neighbors[0] = tiles[0][0];
        tiles[1][0].neighbors[1] = tiles[1][1];
        tiles[1][0].neighbors[2] = tiles[2][0];
        
        tiles[1][1].neighbors[0] = tiles[0][1];
        tiles[1][1].neighbors[1] = tiles[1][2];
        tiles[1][1].neighbors[2] = tiles[2][1];
        tiles[1][1].neighbors[3] = tiles[1][0];
        
        tiles[1][2].neighbors[0] = tiles[0][2];
        tiles[1][2].neighbors[1] = tiles[1][3];
        tiles[1][2].neighbors[2] = tiles[2][2];
        tiles[1][2].neighbors[3] = tiles[1][1];
        
        tiles[1][3].neighbors[0] = tiles[0][3];
        tiles[1][3].neighbors[2] = tiles[2][3];
        tiles[1][3].neighbors[3] = tiles[1][2];
        
        tiles[2][0].neighbors[0] = tiles[1][0];
        tiles[2][0].neighbors[1] = tiles[2][1];
        tiles[2][0].neighbors[2] = tiles[3][0];
        
        tiles[2][1].neighbors[0] = tiles[1][1];
        tiles[2][1].neighbors[1] = tiles[2][2];
        tiles[2][1].neighbors[2] = tiles[3][1];
        tiles[2][1].neighbors[3] = tiles[2][0];
        
        tiles[2][2].neighbors[0] = tiles[1][2];
        tiles[2][2].neighbors[1] = tiles[2][3];
        tiles[2][2].neighbors[2] = tiles[3][2];
        tiles[2][2].neighbors[3] = tiles[2][1];
        
        tiles[2][3].neighbors[0] = tiles[1][3];
        tiles[2][3].neighbors[2] = tiles[3][3];
        tiles[2][3].neighbors[3] = tiles[2][2];
        
        tiles[3][0].neighbors[0] = tiles[2][0];
        tiles[3][0].neighbors[1] = tiles[3][1];
        
        tiles[3][1].neighbors[0] = tiles[2][1];
        tiles[3][1].neighbors[1] = tiles[3][2];
        tiles[3][1].neighbors[3] = tiles[3][0];
        
        tiles[3][2].neighbors[0] = tiles[2][2];
        tiles[3][2].neighbors[1] = tiles[3][3];
        tiles[3][2].neighbors[3] = tiles[3][1];
        
        tiles[3][3].neighbors[0] = tiles[2][3];
        tiles[3][3].neighbors[3] = tiles[3][2];
    }    

    private char[][] toCharArray() {
        char[][] b = new char[4][4];

        for (int row = 0; row < tiles.length; row++) {
            for (int col = 0; col < tiles[row].length; col++) {
                b[row][col] = tiles[row][col].getValue();
            }
        }
        return b;
    }

    private void setTilesFrom(char[][] board) {
        emptyTiles.clear();
        for (int row = 0; row < board.length; row++) {
            for (int col = 0; col < board[row].length; col++) {
                tiles[row][col].value = board[row][col];
                if (board[row][col] == EMPTY_TILE) {
                    emptyTiles.add(tiles[row][col]);
                }
            }
        }
    }

    public class Tile {

        private int row;
        private int col;
        boolean merged = false;
        Tile[] neighbors = new Tile[4];
        private char value;

        Tile(int row, int col) {
            this.row = row;
            this.col = col;
        }

        @Override
        public int hashCode() {
            int hash = 3;
            hash = 97 * hash + this.getRow();
            hash = 97 * hash + this.getCol();
            return hash;
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Tile)) {
                return false;
            }
            Tile t = (Tile) o;
            return this.getRow() == t.getRow() && this.getCol() == t.getCol();
        }

        boolean isNeighbor(Tile target) {
            for (Tile t : neighbors) {
                if (target.equals(t)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * @return the row
         */
        public int getRow() {
            return row;
        }

        /**
         * @return the col
         */
        public int getCol() {
            return col;
        }

        /**
         * @return the value
         */
        public char getValue() {
            return value;
        }
    }

    // <editor-fold defaultstate="collapsed" desc="Property Change Support methods and fields">
    //Proerty Change Listener Support code Begin
    private PropertyChangeSupport mPcs = new PropertyChangeSupport(this);

    /**
     * Add a PropertyChangeListener to the listener list.
     *
     * @see
     * java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener)
     * @param listener The PropertyChangeListener to be added
     */
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        mPcs.addPropertyChangeListener(listener);
    }

    /**
     * Remove a PropertyChangeListener from the listener list.
     *
     * @see
     * java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener)
     * @param listener The PropertyChangeListener to be removed
     */
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        mPcs.removePropertyChangeListener(listener);
    }

    void updateBoard(char[][] board) {
        mPcs.firePropertyChange(Properties.BOARD_CHANGED, null, board);
    }

    void sendNewTile(int x, int y) {
        mPcs.firePropertyChange(Properties.NEW_TILE_COORDS, null, new Point(x, y));
    }

    void sendNewTile(Tile t) {
        mPcs.firePropertyChange(Properties.NEW_TILE, null, t);
    }

    void addToScore(int increase) {
        int old = this.score;
        mPcs.firePropertyChange(Properties.SCORE_CHANGED, old, (this.score += (increase * 2)));
    }

    void setScore(int newScore) {
        int old = this.score;
        mPcs.firePropertyChange(Properties.SCORE_CHANGED, old, (this.score = newScore));
    }

    void setGamePlayStatus(boolean won) {
        mPcs.firePropertyChange(Properties.GAME_STATUS_CHANGED, null, won);
    }

    void decrementUndoCount(int amount) {
        mPcs.firePropertyChange(Properties.UNDO_COUNT_CHANGED, null, (this.undoCount -= amount));
    }

    void incrementUndoCount(int amount) {
        mPcs.firePropertyChange(Properties.UNDO_COUNT_CHANGED, null, (this.undoCount += amount));
    }

    void setUndoStatus(boolean status) {
        mPcs.firePropertyChange(Properties.UNDO_STATUS_CHANGED, null, status);
    }

    void setUndoCount(int amount) {
        mPcs.firePropertyChange(Properties.UNDO_COUNT_CHANGED, null, amount);
    }

    /**
     * This class holds the list of constants that represent the Property Names
     * used by the PropertySuppoertManager.
     */
    public class Properties {

        public static final String BOARD_CHANGED = "changed";
        public static final String GAME_STATUS_CHANGED = "status";
        public static final String NEW_TILE_COORDS = "new";
        public static final String NEW_TILE = "tile";
        public static final String SCORE_CHANGED = "score";
        public static final String UNDO_COUNT_CHANGED = "undocount";
        public static final String UNDO_STATUS_CHANGED = "undostat";

    }

     // </editor-fold>
}
